﻿using ArchestrA.Core;
using ArchestrA.MxAccess;
using Nancy;
using Nancy.Diagnostics;
using Nancy.Hosting.Self;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace aaAttributeBrowser
{
    public class aaAttributeManager : IDisposable
    {
        private static aaAttributeManager _singleton = new aaAttributeManager();
        private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        private Dictionary<int, LMXValue> _lmxValues = new Dictionary<int, LMXValue>(); //maps handles to LMXValues
        private List<aaObject> _objects = new List<aaObject>(); //all of the objects we are tracking
        private int _hLmx; //handle to our registered instance of the LMX server
        private ArchestrA.MxAccess.LMXProxyServerClass _lmxServer; //LMX COM object
        private Timer _t; //timer for doing period cleaning out of the object cache
        private Dictionary<string, object> _customObjects = new Dictionary<string, object>();

        public aaAttributeManager()
        {
            //If we can't do the following, then we can't continue anyway so let the caller handle the error
            _lmxServer = new ArchestrA.MxAccess.LMXProxyServerClass();
            _hLmx = _lmxServer.Register("aaAttributeBrowser");
            _lmxServer.OnDataChange += new _ILMXProxyServerEvents_OnDataChangeEventHandler(LMX_OnDataChange);
            _t = new Timer(CleanStaleObjects, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(-1));
        }

        private void CleanStaleObjects(object state)
        {
            CleanStaleObjects(false);
            _t.Change(TimeSpan.FromHours(0.5), TimeSpan.FromMilliseconds(-1));
        }

        /// <summary>
        /// Request a one off arbitrary attribute
        /// </summary>
        /// <param name="attribute"></param>
        public void AddCustomObject(string attribute, Action<string> callback)
        {
            lock (_lmxValues)
            {
                LMXValue val = new LMXValue() { Name = attribute, OnceOnly = true, CustomCallback = callback, Purpose = aaLMXValuePurpose.Custom };
                int handle = _lmxServer.AddItem(_hLmx, val.Name);
                _lmxValues.Add(handle, val);
                _lmxServer.Advise(_hLmx, handle);
            }
        }

        /// <summary>
        /// Fetch and remove a custom object
        /// </summary>
        /// <param name="attribute"></param>
        /// <returns></returns>
        public object FetchCustomObject(string attribute)
        {
            if (_customObjects.ContainsKey(attribute)) {
                var obj = _customObjects[attribute];
                _customObjects.Remove(attribute);
                return obj;
            }
            else
                return null;
        }

        /// <summary>
        /// Get a reference to the current aaAttributeManager (used by Nancy)
        /// </summary>
        public static aaAttributeManager Singleton
        {
            get
            {
                return _singleton;
            }
        }
        
        /// <summary>
        /// Removes an object from the object cache
        /// </summary>
        /// <param name="objectName"></param>
        public bool RemoveObject(string objectName)
        {
            try
            {
                _log.Info(string.Format("Delete request recieved for {0}", objectName));
                aaObject obj = _objects.FirstOrDefault((arg) => arg.Name == objectName);
                if (obj != null)
                {
                    RemoveObject(obj);
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                _log.Error(ex.ToString());
                return false;
            }
        }

        /// <summary>
        /// Get a JSON representation of an aaObject
        /// </summary>
        /// <param name="ObjectName"></param>
        /// <returns></returns>
        public string GetObjectJSON(string ObjectName)
        {
            aaObject obj = GetObject(ObjectName);

            if (obj == null)
            {
                return "";
            }
            else
            {
                lock (_objects)
                {
                    return JsonConvert.SerializeObject(obj);
                }
            }
        }

        /// <summary>
        /// Unadvise and remove all items, and unregister the server
        /// </summary>
        public void Dispose()
        {
            try
            {
                if (_lmxServer != null)
                {
                    foreach (var kvp in _lmxValues)
                    {
                        try
                        {
                            _lmxServer.UnAdvise(_hLmx, kvp.Key);
                            _lmxServer.RemoveItem(_hLmx, kvp.Key);
                        }
                        catch { };
                    }
                    _lmxServer.Unregister(_hLmx);
                }
            }
            catch (Exception ex)
            {
                _log.Error(ex.ToString());
            }
        }

        public string ObjectCache()
        {
            lock (_objects)
            {
                var results = from a in _objects select new { a.Name, a.LastAccessed };
                return JsonConvert.SerializeObject(results);
            }
        }

        public string EmptyCache()
        {
            return string.Format("The cache has been emptied ({0} objects)",CleanStaleObjects(true));
        }

        /// <summary>
        /// Remove any objects not accessed within the expiry period.
        /// </summary>
        /// <param name="state"></param>
        private int CleanStaleObjects(bool all)
        {
            int count = 0;
            try
            {
                _log.Info("Object cache purge started.");
                List<aaObject> objectsToClean = new List<aaObject>();
                lock (_objects)
                {
                    foreach (var obj in _objects)
                    {
                        if (DateTime.Now - obj.LastAccessed > TimeSpan.FromHours(0.5) || all)
                        {
                            objectsToClean.Add(obj);
                        }
                    }
                }
                count = objectsToClean.Count;

                foreach (var obj in objectsToClean)
                {
                    //remove the object from the list of objects
                    _log.Info(string.Format("Removing {0} from the objects list as it hasn't been used since {1}.", obj.Name, obj.LastAccessed.ToString()));
                    RemoveObject(obj);
                }
                return count;
            }
            catch (Exception ex)
            {
                _log.Error(ex.ToString());
                return count;
            }
        }

        private void LMX_OnDataChange(int hLMXServerHandle, int phItemHandle, object pvItemValue, int pwItemQuality, object pftItemTimeStamp, ref MXSTATUS_PROXY[] pVars)
        {
            _log.Debug("Enter LMX_OnDataChange");
            try
            {
                lock (_objects)
                {
                    if (pVars[0].success != -1)
                    {
                        //uh oh, we have a problem
                        _log.Warn(string.Format("LMX_OnDataChange data error retrieving value {0}: {1}", _lmxValues[phItemHandle].Name, Enum.GetName(typeof(MxStatusDetail), (MxStatusDetail)pVars[0].detail)));
                        if (_lmxValues[phItemHandle].Name.EndsWith("_Attributes[]") && (MxStatusDetail)pVars[0].detail == MxStatusDetail.MX_E_InvalidReference)
                        {
                            _log.Warn(string.Format("Item {0} doesn't exist.", _lmxValues[phItemHandle].Parent.Name));
                            _lmxValues[phItemHandle].Parent.DoesntExist = true;
                            RemoveItem(phItemHandle);
                        }

                        if ((MxStatusDetail)pVars[0].detail == MxStatusDetail.MX_E_IndexOutOfRange)
                        {
                            //TODO: Automatically convert arrays
                            _log.Warn(string.Format("LMX_OnDataChange: {0} is an array type, add [] to the attribute name to get values", _lmxValues[phItemHandle].Name));
                        }
                        return;
                    }
                    if (pvItemValue == null)
                    {
                        _log.Warn(string.Format("LMX_OnDataChange data error retrieving value {0}: value is null", _lmxValues[phItemHandle].Name));
                    }
                    Type objectType = pvItemValue.GetType();
                    if (_lmxValues[phItemHandle].Purpose == aaLMXValuePurpose.Custom)
                    {
                        //TODO: handle this separately
                        if (_customObjects.ContainsKey(_lmxValues[phItemHandle].Name))
                        {
                            _customObjects[_lmxValues[phItemHandle].Name] = pvItemValue;
                        }
                        else
                        {
                            _customObjects.Add(_lmxValues[phItemHandle].Name, pvItemValue);
                        }
                        _lmxValues[phItemHandle].CustomCallback(_lmxValues[phItemHandle].Name);
                    }
                    else
                    {
                        if (!objectType.IsArray)
                        {
                            _lmxValues[phItemHandle].Attribute.Type = objectType;
                            _log.Debug("Recieved " + _lmxValues[phItemHandle].Purpose.ToString() + " data for " + _lmxValues[phItemHandle].Attribute.Name);
                            switch (_lmxValues[phItemHandle].Purpose)
                            {
                                case aaLMXValuePurpose.Description:
                                    _lmxValues[phItemHandle].Attribute.Description = pvItemValue.ToString();
                                    break;
                                case aaLMXValuePurpose.OffLabel:
                                    _lmxValues[phItemHandle].Attribute.OffLabel = pvItemValue.ToString();
                                    break;
                                case aaLMXValuePurpose.OnLabel:
                                    _lmxValues[phItemHandle].Attribute.OnLabel = pvItemValue.ToString();
                                    break;
                                case aaLMXValuePurpose.EngUnit:
                                    _lmxValues[phItemHandle].Attribute.EngUnit = pvItemValue.ToString();
                                    break;
                                case aaLMXValuePurpose.InAlarm:
                                    _lmxValues[phItemHandle].Attribute.InAlarm = (bool)pvItemValue;
                                    break;
                                default:
                                    _lmxValues[phItemHandle].Attribute.SetValue(pvItemValue);
                                    _lmxValues[phItemHandle].Attribute.Quality = pwItemQuality;
                                    _lmxValues[phItemHandle].Attribute.TimeStamp = DateTime.Parse((string)pftItemTimeStamp);
                                    break;
                            }
                        }
                        else
                        {
                            //received data is an array
                            if (_lmxValues[phItemHandle].Purpose == aaLMXValuePurpose.AttributeList)
                            {
                                _log.Debug("Received and processing attributes for " + _lmxValues[phItemHandle].Parent.Name);
                                ProcessAttributes(phItemHandle, pvItemValue);
                            }
                            else
                            {
                                //TODO: do something with these
                            }

                        }
                    }
                    if (_lmxValues[phItemHandle].OnceOnly)
                    {
                        RemoveItem(phItemHandle);
                    }
                }
            }
            catch (Exception ex)
            {
                _log.Error(ex.ToString());
            }
        }

        /// <summary>
        /// Unadvises and removes an attribute
        /// </summary>
        /// <param name="phItemHandle"></param>
        private void RemoveItem(int phItemHandle)
        {
            lock (_lmxValues)
            {
                _lmxServer.UnAdvise(_hLmx, phItemHandle);
                _lmxServer.RemoveItem(_hLmx, phItemHandle);
                _lmxValues.Remove(phItemHandle);
            }
        }
        
        private void RemoveObject(aaObject obj)
        {
            lock (_objects) { 
            _objects.Remove(obj);
                //go through each of the object attributes, and unadvise all the handles
                foreach (var attrib in obj.Attributes)
                {
                    var res = from kvp in _lmxValues where kvp.Value.Attribute == attrib select kvp;
                    foreach (var kvp in res.ToList())
                    {
                        //LogMessage(string.Format("Removing/Unadvising {0} from the lmxValues list.", kvp.Value.Name));
                        RemoveItem(kvp.Key);
                    }
                }
            }
        }

        private int AttributeComparer(aaAttribute x, aaAttribute y)
        {
            return string.Compare(x.Name, y.Name);
        }

        private void ProcessAttributes(int phItemHandle, object pvItemValue)
        {
            lock (_lmxValues)
            {
                List<string> attribs = new List<string>();
                Array pvArray = (Array)pvItemValue;
                for (int i = pvArray.GetLowerBound(0); i < pvArray.GetUpperBound(0); i++)
                {
                    string item = pvArray.GetValue(i).ToString();
                    attribs.Add(item);
                }
                //TODO: more attribute filtering options here
                var res = from t in attribs where t.Contains("InputSource") select t;

                foreach (var t in res)
                {
                    //get the root attribute name
                    string name = t.Substring(0, t.Length - 12);
                    //the fully qualified attribute name
                    string fqName = _lmxValues[phItemHandle].Parent.Name + "." + name;
                    //add the item and store its handle
                    int hItem = _lmxServer.AddItem(_hLmx, fqName);
                    _lmxServer.Advise(_hLmx, hItem);
                    //create the corresponding aaAttribute for the item
                    aaAttribute attrib = new aaAttribute() { Name = fqName };
                    attrib.SetValue("Initializing...");
                    //add the LMXValue to the dictionary, and attach the attribute to the value
                    _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.Normal, Attribute = attrib });
                    //add the attribute to the parent object
                    _lmxValues[hItem].Parent.Attributes.Add(attrib);
                    _lmxValues[hItem].Parent.Attributes.Sort(AttributeComparer);

                    //see if this value has on/off labels, descriptions etc
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".Description")))
                    {
                        //process description
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".Description";
                        //add the item and store its handle
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _log.Debug("Advising Description for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.Description, Attribute = attrib, OnceOnly = true });
                    }
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".OffMsg")))
                    {
                        //process offlabel
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".OffMsg";
                        //add the item and store its handle
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _log.Debug("Advising OffMsg for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.OffLabel, Attribute = attrib, OnceOnly = true });
                    }
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".OnMsg")))
                    {
                        //process onlabel
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".OnMsg";
                        //add the item and store its handle
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _log.Debug("Advising OnMsg for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.OnLabel, Attribute = attrib, OnceOnly = true });
                    }
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".EngUnits")))
                    {
                        //process onlabel
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".EngUnits";
                        //add the item and store its handle
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _log.Debug("Advising EngUnits for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.EngUnit, Attribute = attrib, OnceOnly = true });
                    }
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".InAlarm")))
                    {
                        //process inalarm (discrete values)
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".InAlarm";
                        //add the item and store its handle
                        _log.Debug("Advising InAlarm for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.InAlarm, Attribute = attrib, OnceOnly = false });
                    }
                    if (!string.IsNullOrEmpty(attribs.FirstOrDefault((s) => s == name + ".AlarmMostUrgentInAlarm")))
                    {
                        //process inalarm (analog values)
                        fqName = _lmxValues[phItemHandle].Parent.Name + "." + name + ".AlarmMostUrgentInAlarm";
                        //add the item and store its handle
                        hItem = _lmxServer.AddItem(_hLmx, fqName);
                        _log.Debug("Advising AlarmMostUrgentInAlarm for " + _lmxValues[phItemHandle].Parent.Name + "." + name);
                        _lmxServer.Advise(_hLmx, hItem);
                        //add the LMXValue to the dictionary, and attach the attribute to the value
                        _lmxValues.Add(hItem, new LMXValue { Name = fqName, Parent = _lmxValues[phItemHandle].Parent, Purpose = aaLMXValuePurpose.InAlarm, Attribute = attrib, OnceOnly = false });
                    }
                }
            }
        }
    
        /// <summary>
        /// This returns a dictionary of base attributes, each with a list of all 'sub attributes' that belong to it
        /// </summary>
        /// <param name="attributes"></param>
        private List<Attribute> ProcessAttributesSomeMore(List<string> attributes)
        {
            attributes.Sort();
            Dictionary<string, List<string>> uniqueAttributes = new Dictionary<string, List<string>>();
            string previous = "zoogabooga";//I bet nobody will call an attribute this
            List<string> currentList = new List<string>();
            for (int i = 0; i < attributes.Count; i++)
            {
                if (attributes[i].StartsWith(previous))
                {
                    currentList.Add(attributes[i]);
                }
                else
                {
                    currentList = new List<string>();
                    uniqueAttributes.Add(attributes[i], currentList);
                    previous = attributes[i];
                }

            }

            //loop through the list, and convert each entry into an attribute

            return null;
        }

        private aaObject GetObject(string ObjectName)
        {
            try
            {
                _log.Info("Fetching object " + ObjectName);
                //If we have already retrieved the attributes for an object, then just return the values
                aaObject obj = _objects.FirstOrDefault((arg) => arg.Name == ObjectName);
                if (obj != null)
                {
                    if (obj.DoesntExist)
                    {
                        return null;
                    }
                    else
                    {
                        obj.LastAccessed = DateTime.Now;
                        return obj;
                    }
                }
                //we are fetching a new object
                obj = new aaObject() { Name = ObjectName, Attributes = new List<aaAttribute>()};
                lock (_objects)
                {
                    lock (_lmxValues)
                    {
                        //otherwise fetch the attributes
                        //Pro tip: put the brackets on the end to get an array (someone should put this in the documentation for MXAccess...)
                        _log.Debug("Advising  " + ObjectName + "._Attributes[]");
                        LMXValue val = new LMXValue() { Name = ObjectName + "._Attributes[]", OnceOnly = true, Parent = obj };
                        int hAttribs = _lmxServer.AddItem(_hLmx, val.Name);
                        _lmxValues.Add(hAttribs, val);
                        _lmxServer.Advise(_hLmx, hAttribs);
                    }
                    _objects.Add(obj);
                }
                //Give the initial values time to come through on the other threads
                return ObjectAttributesReady(obj);
            }
            catch (Exception ex)
            {
                _log.Error(ex.ToString());
                return null;
            }
        }

        private aaObject ObjectAttributesReady(aaObject obj)
        {
            //TODO: we need to return more information here to differentiate between not found, timed out/still initialising, and error
            int i = 0;
            while (i < 8)
            {
                if (obj.DoesntExist)
                    return null;
                if (obj.Attributes.Count > 0)
                    return obj;
                Thread.Sleep(500); //TODO: this should be refactored out to a command line switch
                i++;
            };
            //if nothing comes through by ~2s then assume that nothing is ok
            _log.Debug("Over 2s elapsed from advising attributes for " + obj.Name);
            return null;
        }
    }
}
